feat: add CraftDImage component for Android/KMP#102
feat: add CraftDImage component for Android/KMP#102rviannaoliveira wants to merge 21 commits intomainfrom
Conversation
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add OpenSpec commands and skills (propose, explore, apply, archive) - Add Android/Compose skills (testing, performance, accessibility, gradle, compose-ui) - Update CLAUDE.md with folder patterns, code principles and docs rule - Add GitHub Copilot equivalents (prompts and skills) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add PostToolUse hook that triggers Claude review after gh pr create - Add review-pr.sh script with checklist based on CLAUDE.md rules - Add PR review section to CLAUDE.md with review guidelines Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Rules derived from PR #78 review comments: - Rule 9: every new builder must be registered in CraftDBuilderManager - Rule 10: external library dependencies must be abstracted via interface/parameter Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Rule belongs in propose.md, not in project-level instructions. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ifacts Scoped change to Android/KMP only (iOS and Flutter out of scope). Generated proposal.md, design.md, specs/craftd-image/spec.md and tasks.md. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Rule 11: every new component must have a working example in the corresponding sample app (app-sample-android for Compose and XML). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add section 6 with tasks to register and demonstrate CraftDImage in app-sample-android for both Compose and XML setups. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Remove PostToolUse/Edit hook and build-on-task-complete.sh. Add explicit rule: run ./gradlew build before marking task [x]. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Define parallel agent strategy in CLAUDE.md: core (sequential) → compose+xml (parallel) → docs/sample → revisor. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add IMAGE_COMPONENT to CraftDComponentKey enum - Add CraftDContentScale enum (CROP, FIT, FILL_BOUNDS, FILL_WIDTH, FILL_HEIGHT, INSIDE, NONE) - Add ImageProperties data class with url, contentScale, contentDescription, actionProperties Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Compose: - Add toContentScale() extension mapping CraftDContentScale → ContentScale - Add CraftDImage composable with injectable imageLoader lambda - Add CraftDImageBuilder with injectable imageLoader constructor param XML: - Add CraftDImageComponent (AppCompatImageView wrapper) - Add CraftDImageComponentRender with injectable imageLoader - Register CraftDImageComponentRender in CraftDBuilderManager via optional imageLoader param Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Unit tests for ImageProperties serialization, toContentScale() and CraftDImageBuilder key - Updated docs/how-to-use/compose.md and view-system.md with CraftDImage usage examples - Registered CraftDImageBuilder (Coil) in Compose sample and CraftDImageComponentRender (Picasso) in XML sample - Added CraftDImage entry to dynamic.json - Switched app-sample-android to local craftd-xml project dependency - Added Coil 2.6.0 to libs.versions.toml - Deleted notes.md after context consumed Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
rviannaoliveira
left a comment
There was a problem hiding this comment.
Review — feat: add CraftDImage component for Android/KMP
Resumo geral
Implementação bem estruturada no geral — separação de plataformas respeitada, abstração de imageLoader correta, sample funcional, docs atualizados. Porém há um problema de comportamento que precisa ser corrigido antes do merge e alguns pontos menores.
🔴 Bloqueador — contentScale definido no model mas nunca utilizado
ImageProperties.contentScale existe no modelo e é documentado, mas não é passado em nenhuma das implementações:
Compose (CraftDImage.kt): o imageLoader recebe (url, contentDescription, modifier) — sem ContentScale. Há até uma função toContentScale() criada em UtilsCompose.kt, mas ela não é chamada em lugar nenhum no composable.
XML (CraftDImageComponentRender.kt): o imageLoader recebe (url, imageView) — contentScale completamente ignorado.
O resultado é que o servidor pode enviar "contentScale": "CROP" e nada acontece. Correto seria incluir ContentScale na assinatura do imageLoader do Compose e um valor equivalente (ImageView.ScaleType) no XML, ou ao menos aplicar internamente quando o imageLoader não precisar controlar isso.
🟡 Atenção — url nullable com fallback silencioso
Em CraftDImage.kt:19:
imageLoader(properties.url.orEmpty(), ...)Se url for null, chama o imageLoader com string vazia. Dependendo do imageLoader (Coil, Picasso, Glide), isso pode gerar erro em runtime ou exibir imagem errada silenciosamente. O correto é guardar contra null e não renderizar:
val url = properties.url ?: return
imageLoader(url, ...)🟡 Atenção — CraftDImageBuilder não registrado no CraftDBuilderManager do Compose (Regra 9)
A PR descreve isso como "not pre-registered by design" — o que faz sentido dado que imageLoader é obrigatório na construção (Regra 10). Mas a Regra 9 exige registro em CraftDBuilderManager. Sugiro um de dois caminhos:
- Documentar no
CLAUDE.mdque builders com dependências externas obrigatórias são exceção à Regra 9 (precisam ser registrados pelo consumidor), ou - Tornar
imageLoaderopcional com uma implementação padrão no-op para permitir registro default
Sem essa resolução, a regra fica inconsistente.
🟡 Atenção — Ausência de testes para CraftDImageComponentRender (XML)
Há testes para ImageProperties, ContentScaleExtension e CraftDImageBuilder, mas nenhum para CraftDImageComponentRender. A Regra 7 exige testes quando possível. Ao menos um teste verificando que bindView chama imageLoader com a URL correta e que o click listener é registrado quando actionProperties não é null seria suficiente.
✅ O que está correto
- Regra 1 — Nenhuma dependência entre módulos de plataforma
- Regra 2 —
CraftDImageBuilderimplementaCraftDBuilder;CraftDImageComponentRenderimplementaCraftDViewRenderer - Regra 3 —
onActioncoberto com fallback em ambas as plataformas (actionProperties?.let { ... }) - Regra 6 — Nome
CraftDImageconsistente entre Compose e XML - Regra 8 —
docs/how-to-use/compose.mdeview-system.mdatualizados - Regra 9 — XML:
CraftDImageComponentRenderregistrado corretamente viaimageLoader?.let { ... }nogetBuilderRenders() - Regra 10 —
imageLoaderinjetável, sem acoplamento direto a Coil/Picasso/Glide - Regra 11 — Sample demonstra o componente em Compose (Coil) e XML (Picasso)
- Testes nomenclatura — backtick correto:
`given X when Y then Z` dynamic.jsonincluiCraftDImagecomactionPropertiespara validar tap ✓
rviannaoliveira
left a comment
There was a problem hiding this comment.
Review — CraftDImage Android/KMP
✅ Aprovado com observações
Arquitetura
CraftDImageBuilderimplementaCraftDBuildercorretamente ✓CraftDImageComponentRenderestendeCraftDViewRenderercorretamente ✓- Sem dependência entre módulos de plataforma ✓
onActioncoberto em Compose e XML ✓imageLoadercorretamente abstraído via lambda — Coil/Picasso nunca entram nos builders ✓- Builder XML registrado via parâmetro opcional
imageLoaderemgetBuilderRenders()✓ CraftDImageBuilderCompose não pré-registrado por design (requer injeção deimageLoader) ✓
Testes
- Nomenclatura em backtick ✓
ImagePropertiesTestcobre serialização/desserialização ✓ContentScaleExtensionTestcobre todos os 7 valores + null ✓CraftDImageBuilderTestcobre apenas a propriedadekey— comportamento doimageLoadereactionPropertiesnão testados. Limitação conhecida pela ausência de Compose UI test infra no projeto.
Docs
compose.mdeview-system.mdatualizados ✓
Observações
1. Alias no libs.versions.toml
coil_compose usa underscore mas a convenção do catalog é hífen (coil-compose), que gera o accessor libs.coil.compose de forma mais legível. Não é blocker, mas vale ajustar para consistência com os demais aliases.
2. @Stable/@Immutable em commonMain
ImageProperties e CraftDContentScale usam anotações do compose.runtime em commonMain. O Compose Multiplatform Runtime é KMP-safe, então tecnicamente não viola a regra de commonMain sem deps de plataforma. Mas vale ter ciência que isso acopla o model ao Compose — se no futuro o core precisar ser usado sem Compose, seria necessário remover essas anotações.
3. app-sample-android agora usa projects.craftdXml local
A dependência io.github.codandotv:craftd-xml:1.1.0 foi removida e substituída pelo projeto local. Correto para que o sample use a versão com imageLoader, mas vale garantir que o CI tem o ambiente para buildar o módulo local.
|
Substituído pelo PR #103 com branch limpa a partir do main. |

Summary
CraftDImagecomponent tocraftd-core,craftd-composeandcraftd-xmlImagePropertiesmodel withurl,contentScale,contentDescriptionandactionPropertiesCraftDContentScaleenum mapping to ComposeContentScaleCraftDImageBuilder(Compose) with injectableimageLoader— not pre-registered by designCraftDImageComponentRender(XML) with injectableimageLoader— registered inCraftDBuilderManagerImagePropertiesserialization,toContentScale()andCraftDImageBuilderkeycompose.mdandview-system.md.claude/instructions/,platform:frontmatter rule for/proposeTest plan
./gradlew :craftd-core:testDebugUnitTestpasses./gradlew :craftd-compose:testDebugUnitTestpasses./gradlew :app-sample-android:assembleDebugpasses🤖 Generated with Claude Code